"""Classes for loading, generating and rendering terrain."""

__author__ = 'Josh Oosterman'

import os
import pygame
import pygame.transform
import re

from potv import engine
from potv import geom


TERRAIN_SIZE = 128
ROAD_SIZE = 256

def _PreprocessMatrix(matrix, valid_values):
  """Calculate matrix dimensions, and transpose it."""
  width, height = len(matrix[0]), len(matrix)
  result = []
  for x in xrange(width):
    result.append([])
    for y in xrange(height):
      result[x].append([])
      val = matrix[y][x]
      if not val in valid_values:
        raise ValueError('Invalid tile id %d' % val)
      result[x][y] = val
  return result, width, height


def _BuildMatchList(matches):
  """Extend a match list to contain all rotations."""
  result = []
  for pattern, filename in matches:
    for rot in xrange(0, 4):
      result.append((pattern.replace('*', '.'), (filename, rot)))
      pattern = '%s%s' % (pattern[2:], pattern[:2])
  return result


def _Match(matches, matrix, x, y, current):
  """Calculate tile based on regex matching with surrounding elements.

  Args:
    matches: A list of (regexes, image key) tuples.
    matrix: A matrix, each element is an integer layer value.
    x: An integer, the x index.
    y: An integer, the y index.
    current: An integer, the current layer value to match.

  Returns:
    An image key, which is a tuple of image_name, rotation.
  """
  vectors = [
      (0, -1),(1, -1), (1, 0), (1, 1), (0, 1), (-1, 1), (-1, 0), (-1, -1)]

  # Build string to describe surrounding tiles.
  bits = []
  for vect in vectors:
    other = GetElement(matrix, x + vect[0], y + vect[1])
    bits.append('1' if other == current and other > 0 else '0')
  surrounding = ''.join(bits)

  # Check string against match dict.
  for match, img_key in matches:
    if re.match(match, surrounding):
      return img_key


class TerrainBuilder(object):
  """Builds the terrain tile image matrix."""

  LAYERS = ['water', 'sand', 'grass']
  DEFAULT = 'plain'
  MATCHES = [
      ('1*1*1*1*', '4_edges'),
      ('01010101', '4_corners'),
      ('0*1*1*1*', '3_edges'),
      ('01000101', '3_corners'),
      ('00000101', '2_corners_adjacent'),
      ('01000100', '2_corners_opposite'),
      ('0*1*0*1*', '2_edges_opposite'),
      ('000*1*1*', '2_edges_adjacent'),
      ('010*1*1*', '2_edges_opposite_corner'),
      ('01000*1*', '1_edge_opposite_corner'),
      ('00010*1*', '1_edge_opposite_corner_2'),
      ('01010*1*', '1_edge_2_opposite_corners'),
      ('****010*', '1_corner'),
      ('00000*1*', '1_edge'),
      # TODO(joosterman): Add default match for debugging.
  ]
  FILEPATH_PATTERN = 'terrain/%s/%s.png'

  def __init__(self, terrain_matrix):
    self.terrain_matrix = terrain_matrix

  def Build(self):
    """Builds the terrain tile image matrix."""
    valid_layers = range(0, len(self.LAYERS))
    self.terrain_matrix, self.width, self.height = \
        _PreprocessMatrix(self.terrain_matrix, valid_layers)
    self._LoadTerrainImages()
    return self._CalculateTerrainTiles()

  def _LoadTerrainImages(self):
    """Loads all terrain tile images from disk."""
    self.terrain_images = []
    self.terrain_edges = {}

    for i in xrange(len(self.LAYERS)):
      layer = self.LAYERS[i]

      # Load default terrain tile image.
      img = engine.LoadImage(self.FILEPATH_PATTERN % (layer, self.DEFAULT))
      img.CenterOrigin()
      self.terrain_images.append(img)

      # Load terrain edge tile images.
      self.terrain_edges[i] = {}
      for _, tile_name in self.MATCHES:
        filename = self.FILEPATH_PATTERN % (layer, tile_name)
        img = engine.LoadImage(filename)
        if not img:
          continue
        img.CenterOrigin()
        for rot in xrange(0, 4):
          self.terrain_edges[i][(tile_name, rot)] = img
          img = img.Rotated(90)


  def _CalculateTerrainTiles(self):
    """Calculate lists of tile images to display at each x, y."""
    matches = _BuildMatchList(self.MATCHES)
    result = []

    # First set each base tile.
    for x in xrange(self.width):
      result.append([])
      for y in xrange(self.height):
        result[x].append([])
        img = self.terrain_images[self.terrain_matrix[x][y]]
        result[x][y].append(img)

    # Now calculate edge tiles by matching the self.match regexes against the
    # surrounding tiles.
    for x in xrange(self.width):
      for y in xrange(self.height):
        layer = GetElement(self.terrain_matrix, x, y) + 1
        for i in xrange(layer, len(self.LAYERS)):
          img_key = _Match(matches, self.terrain_matrix, x, y, i)
          img = self.terrain_edges[i].get(img_key, None)
          if img:
            result[x][y].append(img)

    return result


class RoadBuilder(object):
  """Takes a simple terrain matrix and computes road tiles."""

  FILEPATH_PATTERN = 'road/%s/%s.png'
  MATCHES = [
      ('0*1*0*1*', 'plain'),
      ('1*0*0*1*', 'corner'),
      ('1*1*0*1*', 'tintersection'),
      ('0*1*0*0*', 'cauldersack'),
      ('1*1*1*1*', 'xintersection'),
      ('********', 'xintersection') # Use x intersection as default.
  ]

  def __init__(self, road_type, road_matrix):
    self.road_type = road_type
    self.road_matrix = road_matrix

  def Build(self):
    """Builds the road tile image matrix."""
    self.road_matrix, self.width, self.height = \
        _PreprocessMatrix(self.road_matrix, [0, 1])
    self._LoadRoadImages()
    return self._CalculateRoadTiles()

  def _LoadRoadImages(self):
    """Loads all road tile images from disk."""
    self.road_images = {}

    for _, tile_name in self.MATCHES:
      filename = self.FILEPATH_PATTERN % (self.road_type, tile_name)
      img = engine.LoadImage(filename)
      if not img:
        continue
      img.CenterOrigin()
      for rot in xrange(0, 4):
        self.road_images[(tile_name, rot)] = img
        img = img.Rotated(90)

  def _CalculateRoadTiles(self):
    """Calculate lists of tile images to display at each x, y."""
    matches = _BuildMatchList(self.MATCHES)
    result = []

    # First set each base tile.
    for x in xrange(self.width):
      result.append([])
      for y in xrange(self.height):
        result[x].append([])

    # Now calculate edge tiles by matching the self.match regexes against the
    # surrounding tiles.
    for x in xrange(self.width):
      for y in xrange(self.height):
        if self.road_matrix[x][y]:
          img_key = _Match(matches, self.road_matrix, x, y, 1)
          img = self.road_images.get(img_key, None)
          if img:
            result[x][y].append(img)

    return result

class TileRenderer(object):
  """Renders the visible tiles to the view."""

  def __init__(self, tile_images, tile_size):
    """Initialize the TerrainRenderer."""
    self.tile_images = tile_images
    self.tile_size = float(tile_size)
    self.width, self.height = len(tile_images[0]), len(tile_images)

  def Draw(self, view):
    """Draw the visible terrain to the view."""
    loc = view.world_location / self.tile_size
    x_low, y_low = loc.x, loc.y
    x_high = (x_low + view.rect.width / self.tile_size) + 2
    y_high = (y_low + view.rect.height / self.tile_size) + 2
    for x in xrange(int(x_low), int(x_high)):
      for y in xrange(int(y_low), int(y_high)):
        for img in GetElement(self.tile_images, x, y, []):
          view.DrawImage(img, geom.Vector(x + 0.5, y + 0.5) * self.tile_size)


def GetElement(array, x, y, default=-1):
  """Attempts to get the element, failing at the given position."""
  try:
    return array[x][y]
  except:
    return default
